Effective Java

Author Avatar
罗炜光 4月 08, 2016
  • 在其它设备中阅读本文章

1. 考虑用静态工厂方法代替构造器
优点:

  • 拥有名称,更容易识别
  • 不必每次调用时,都创建一个新的对象
  • 可以返回返回类型的任何子类型对象
  • 使代码更简洁

缺点:

  • 类如果不含共有的或者受保护的构造器,就不能被子类化。
  • 它们与其他的静态方法实际上没有任何区别。

应用:

  • 服务提供者框架

2. 遇到多个构造器参数时要考虑用构建器

  • 当有很多参数的时候,重叠构造器模式可行,客户端代码会很难编写,并且仍然较难以阅读
  • 使用javabeans模式,在构造过程中JavaBean可能处于不一定的状态
  • 使用Builder 模式的客户端代码将更易于阅读与编写,构建器也比JavaBeans更加安全。

3. 用私有构造器或者枚举类型强化Singleton属性

  • 使类成为Singleton会是它的客户端测试变得十分困难,因为无法给Singleton替换模拟实现,除非它实现一个充当其类型的接口。
  • 序列化Singleton类使用implement Serializable是不够的,为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法,否则每次反序列化一个序列化的实例时,都会创建一个新的实例。
  • 单元素的枚举类型已经成为Singleton的最佳方法
    例子:

    public enum Elvis
    {
      INSTANCE;
    
      public void leaveTheBuilding(){};
    }
    

    4. 通过私有构造器强化不可实例化的能力

  • 导致一个类不能被子类化。所有的构造器都必须显示或隐式的调用超类的构造器。如此一来子类就没有可访问的超类构造器可用了

5. 避免创建不必要的对象

  • 使用静态工厂方法而不是构造器,以避免创建不必要的对象
  • 要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

6. 消除过期的对象引用

  • 只要类是自己管理内存,就应该警惕内存泄露问题
  • 内存泄露的另一个常见来源是缓存
  • 内存泄露的第三个常见来源是监听器和其他回调

7. 避免使用终结方法

  • 不应该依赖终结方法来更新重要的持久状态
  • 使用终结方法有一个非常严重的性能损失
  • 如果终结方法发现资源还未被终止,则应该在日志中记录一条警告
  • 如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法,应该在try块中终结子类,并在相应的finally块中调用超类的终结方法

8. 覆盖equals时请遵守通用约定

  • 类的每个实例本质上都是唯一的
  • 不关心类是否提供了”逻辑相等”的测试功能
  • 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的
  • 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用

equals方法实现了等价关系

  • 自反性。对于任何非null的引用值x,x.equals(x)必须返回true
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true.
  • 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致的返回false。
  • 非空性。对于任何非null的引用值x,x.equals(null)必须返回false。

  • 无法在扩展可实例化的类的同时,即增加新的组件,同时又保留equals约定

实现高质量equals方法的诀窍

  • 使用==操作符检查”参数是否为这个对象的引用”
  • 使用instanceof操作符检查”参数是否为正确的类型”
  • 把参数转换成正确的类型
  • 对于该类中的每个”关键””域,检查参数中的域是否与该对象中对应的域相匹配
  • 当编写完equals方法之后,应该问自己三个问题,它是否是对称的,传递的,一致的?

告诫:

  • 覆盖equals时总要覆盖hashCode。
  • 不要企图让equals方法过于智能
  • 不要将equals声明中的Object对象替换为其他的类型

9. 覆盖equals时总要覆盖hashCode

  • 相等的对象必须具有相等的散列码
  • 不要试图从散列码计算中排除掉一个对象的关键部分来提高性能

10. 始终覆盖toString

  • toString()方法应该返回对象中包含的所有值得关注的信息

11. 谨慎的覆盖clone

  • 永远不要让客户去做类库能够替客户完成的事情
  • 必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件

12. 考虑实现Comparable接口

13. 使类和成员的可访问性最小化

  • 尽可能地使每个类或者成员不被外界访问

14. 在公有类中使用访问方法而非公有域

  • 如果类可以在它所在的包的外部进行访问,就提供访问方法。

15. 使可变性最小化

使类成为不可变,要遵循下面五条规则

  • 不要提供改变对象属性的方法
  • 保证类不会被扩展
  • 使所有的域都是final的
  • 使所有的域都成为私有的
  • 确保对于任何可变组件的互斥访问

优点

  • 不可变对象比较简单
  • 不可变对象本质上是线程安全的,它们不要求同步
  • 不可变对象可以被自由地共享
  • 不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
  • 不可变对象为其他对象提供了大量的构件

缺点

  • 对于每个不同的值都需要一个单独的对象、

16. 复合优先于继承

17. 要么为继承而设计,并提供文档说明,要么就禁止继承

  • 构造器决不能调用可被覆盖的方法

18. 接口优于抽象类

  • 现有的类可以很容易被更新,以实现新的接口
  • 接口是定义混合类型的理想选择
  • 接口允许我们构造非层次结构的类型框架
  • 接口使得安全的增强类的功能成为可能
  • 抽象类的演变比接口的演变要容易的多
  • 接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的

19. 接口只用于定义类型

20. 类层次优于标签类

  • 标签类过于冗长、容易出错、并且效率低下

21. 用函数对象表示策略

22. 优先考虑静态成员类

23. 请不要在新代码中使用原生态类型

  • 如果使用原生态类型。就失掉了泛型在安全性和表述性方面的所有优势
  • 如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像
    List这样的参数化类型,则不会

24. 消除非受检警告

  • 要尽可能地消除每一个非受检警告
  • 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个人@SuppressWarnings(“unchecked”)注解来禁止这条警告
  • 应该始终在尽可能小的范围中使用@SuppressWarnings注解
  • 每当使用SuppressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做是安全的

25. 列表优先于数组

  • 数组是协变的(convariant)。相反泛型则是不可变的(invariant)。即如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型;
  • 数组是具体化的(reified)。因此数组会在运行时才知道并检查他们的元素类型约束。泛型是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃(或者擦除)他们元素的类型信息。
  • 创建泛型数组是非法的:

26. 优先考虑泛型

  • 使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。
  • 再设计新类型的时候,要确保他们不需要这种转换就可以使用

27. 优先考虑泛型方法

28. 利用有限制通配符来提升API的灵活性

  • 为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
  • 如果参数化类型表示一个T生产者,就使用《? extends T>,如果它表示一个T消费者,就使用<? super T>
  • 不要使用通配符类型作为返回类型

29. 优先考虑类型安全的异构容器

30. 用enum代替int常量

  • 枚举提供了编译时的类型安全。

31. 用实例域代替序数

  • 永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中

32. 用EnumSet代替位域

33. 用EnumMap代替序数索引

34. 用接口模拟可伸缩的枚举

35. 注解优先于命名模式

命名模式的缺点:

  • 无法处理命名失误的情况。
  • 无法确保它们只用于响应的程序元素上
  • 他们没有提供将参数值与程序元素关联起来的好方法。

36. 坚持使用Override注解

37. 用标记接口定义类型

  • 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型
  • 使用标记接口的方法能够更加精确的对实现它的类型进行锁定。
  • 标记注解胜过标记结构的最大优点在于,他可以通过默认的方式添加一个或多个注解类型元素,给已被使用的注解类型添加更多的信息

38. 检查参数的有效性

39. 必要时进行保护性拷贝

  • 假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
  • 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象
  • 对于参数类型可以被不信任方子类化的参数,请不要使用clone方法进行保护性拷贝

40. 谨慎设计方法签名

  • 谨慎的选择方法的名称
  • 不要过于追求提供便利的方法
  • 避免过长的参数列表
  • 分解成多个方法
  • 创建辅助类
  • 对于参数类型,要优先使用接口而不是类
  • 对于boolean参数,要优先使用两个元素的枚举类型

41. 慎用重载

  • 永远不要导出两个具有相同参数数目的重载方法。
  • 必须要保证当传递同样当参数时,所有的重载方法的行为必须一致。

42. 慎用可变参数

  • 不必改造具有final数组参数的每个方法,只当确定是在数量不定的值上执行调用时才使用可变参数

43. 返回零长度的数组或者集合,而不是null

44. 为所有导出的API元素编写文档注释

45. 将局部变量的作用域最小化

  • 要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明
  • 几乎每个局部变量的声明都应该包含一个初始化表达式

46. for-each循环优先于传统的for循环

三种常见的情况无法使用for-each循环

  • 过滤–如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。
  • 转换–如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值
  • 平行迭代–如果需要并行地遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移

47. 了解和使用类库

48. 如果需要精确的答案,请避免使用float和double

49. 基本类型优先于装箱基本类型

  • 对装箱基本类型运行==操作符几乎总是错误的
  • 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
  • 自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险

基本类型和装箱基本类型有三个主要区别:

  • 基本类型只有值,装箱基本类型则具有与它们的值不同的同一性。
  • 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值null
  • 基本类型通常比装箱基本类型更节省时间和空间。

什么时候应该使用装箱基本类型:

  • 作为集合中的元素,键和值
  • 在参数化类型中,必须使用装箱基本类型作为类型参数
  • 反射的方法调用时,必须使用装箱基本类型

50. 如果其他类型更适合,则尽量避免使用字符串

  • 字符串不适合代替其他的值类型
  • 字符串不适合代替枚举类型
  • 字符串不适合代替聚集类型
  • 字符串不适合代替能力表

51. 当心字符串连接的性能

  • 为连接n个字符串而重复第使用字符串连接操作符,需要n的平方级的时间。

52. 通过接口引用对象

  • 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明

53. 接口优先于反射机制

反射机制的缺点:

  • 丧失了编译时类型检查的好处
  • 执行反射访问所需要的代码非常笨拙与冗长
  • 性能损失

54. 谨慎地使用本地方法

  • 使用本地方法来提高性能的做法不值得提倡

本地方法的三种用途:

  • 它们提供了“访问特定于平台的机制”的能力
  • 提供了访问遗留代码库的能力,从而访问遗留数据。
  • 可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。

55. 谨慎地进行优化

  • 要努力编写好的程序而不是快的程序
  • 努力避免那些限制性能的设计决策
  • 要考虑API设计决策的性能后果
  • 在每次试图做优化之前和之后,要对性能进行测量

56. 遵守普遍接受的命名惯例

57. 只针对异常的情况才使用异常

58. 对可恢复的情况使用受检异常,对编程错误使用运行时异常

  • 如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常
  • 用运行时异常来表面编程错误
  • 实现的所有未受检的抛出结构都应该是RuntimeExecption的子类

59. 避免不必要地使用受检的异常

60. 优先使用标准的异常

61. 抛出与抽象相对应的异常

  • 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法称为异常转译
  • 尽管异常转译与不加选择的从低层传递异常的做法相比有所改进,但是它也不能被滥用

62. 每个方法抛出的异常都要有文档

  • 始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件

63. 在细节消息中包含能捕获失败的而信息

  • 异常的toString方法应该尽可能多地返回有关失败原因的信息
  • 为了捕获失败,异常的细节消息应该包含所有“对该异常有贡献”的参数和域的值

64. 努力使失败保持原子性

65. 不要忽略异常

  • 空的catch块会使异常达不到应有的目的,至少应当包含一条说明,解释为什么可以忽略这个异常

66. 同步访问共享的可变数据

  • 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
  • 如果读和写操作没有都被同步,同步就不会起作用
  • 将可变数据限制在单个线程中

67. 避免过度同步

68. executor和task优先于线程

69. 并发工具优先于wait和notify

70. 线程安全性的文档化

71. 慎用延迟初始化

72. 不要依赖于线程调度器

73. 避免使用线程组

74. 谨慎地实现Serializable接口

  • 为了继承而设计的类应该尽可能少的实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口
  • 对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器

序列化的缺点:

  • 一旦一个类被发布,就大大降低了”改变这个类的实现”的灵活性
  • 它增加了出现Bug和安全漏洞的可能性
  • 随着类发行新的版本,相关的测试负担也增加了

75. 考虑使用自定义的序列化形式

76. 保护性地编写readObject方法

77. 对于实例控制,枚举类型优先于readResolve

78. 考虑用序列化代理代替序列化实例